package net.lucene.chapter1.test;

import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
//@formatter:off

/**
 * 【第一章】HelloWorld入门
 * ------------------------------------------------------------
 *
 * 1)对于全文搜索工具，都是由索引、搜索、分词三部分组成
 * 2)被存储和被索引，是两个独立的概念
 *
 * ------------------------------------------------------------
 *
 * ⊙域的存储选项
 * @see org.apache.lucene.document.Field.Store.YES  会把该域中的内容存储到文件中，方便进行文本的还原
 * @see org.apache.lucene.document.Field.Store.NO   表示该域中的内容不存储到文件中，但允许被索引，且内容无法完全还原
 *
 * ------------------------------------------------------------
 *
 * ⊙域的索引选项
 * @see org.apache.lucene.document.Field.Index.ANALYZED                 进行分词和索引，适用于标题、内容等
 * @see org.apache.lucene.document.Field.Index.NOT_ANALYZED             进行索引但不分词(如ID号，身份证等)，适用于精确搜索
 * @see org.apache.lucene.document.Field.Index.ANALYZED_NO_NORMS        进行分词但不存储norms信息，这个norms中包含了创建索引的时间和权值等信息
 * @see org.apache.lucene.document.Field.Index.NOT_ANALYZED_NO_NORMS    既不进行分词也不存储norms信息
 * @see org.apache.lucene.document.Field.Index.NO                       不进行索引
 *
 * norms当数据被搜索出来后，便涉及到排序的问题，而排序是一种评分规则的，于是NORMS就存储了这些排序的信息
 *
 * ------------------------------------------------------------
 *
 * ⊙域选项最佳实践
 * @see     Field.Store         Field.Index             域值
 *              YES             NOT_ANALYZED_NO_NORMS   标识符(主键、文件名)，电话号码，身份证号，姓名，日期
 *              YES             ANALYZED                文档标题和摘要
 *               NO             ANALYZED                文档正文
 *               NO             NOT_ANALYZED            隐藏关键字
 *              YES             NO                      文档类型，数据库主键(不进行索引)
 *
 *
 * @author Barudisshu
 */
public class HelloWorld {
//@formatter:on

    private static final String PATH_OF_FILE = "chapter1-demo/src/main/resources/lucene/01_file";    // 待索引文件的目录
    private static final String PATH_OF_INDEX = "chapter1-demo/src/main/resources/lucene/01_index";  // 存放索引文件的目录

    //@formatter:off
    /**
     * ---------------------------------------------------------
     * 创建索引
     *
     * 1. 创建Directory，指定索引被保存的位置
     * 2. 创建IndexWriter，通过IndexWriter写索引
     * 3. 创建Document对象，我们索引的有可能是一段文本or数据库中的一张表
     * 4. 为Document添加Field，相当于Document的标题、大小、内容、路径等等，二者类似于数据库表中每条记录和字段的关系
     * 5. 通过IndexWriter添加文档到索引
     * 6. 关闭IndexWriter，用完IndexWriter之后，必须关闭
     *
     * ---------------------------------------------------------
     *
     * _0.fdt和_0.fdx文件--保存域中所存储的数据(Field.Store.YES条件下的)
     * _0.fnm文件----------保存域选项的数据(即new Field(name, value)中的name)
     * _0.frq文件----------记录相同的文件(或查询的关键字)出现的次数,它是用来做评分和排序的
     * _0.nrm文件----------存储一些评分信息
     * _0.prx文件----------记录偏移量
     * _0.tii和_0.tis文件--存储索引里面的所有内容信息
     * segments_1文件------它是段文件,Lucene首先会到段文件中查找相应的索引信息
     *
     * */
    public void createIndex() {
     //@formatter:on

        Directory directory;
        IndexWriter writer = null;
        Document doc;

        try {
            /* FSDirectory会根据当前的运行环境打开一个合理的基于File的Directory(若在内存中创建索引则new RAMDirectory())
               这里将在01_index文件夹创建索引
             */
            directory = FSDirectory.open(new File(PATH_OF_INDEX));

            /* 自lucene2.9之后，其索引格式将不会再兼容Lucene的所有版本，所以在创建索引之前，要指定Lucene的版本
               这里通过IndexWriterConfig构造函数的Version.LUCENE_36指定索引所匹配的版本号，并使用Lucene的标准分词器
             */
            writer = new IndexWriter(directory, new IndexWriterConfig(Version.LUCENE_36,
                    new StandardAnalyzer(Version.LUCENE_36)));

            for (File file : FileUtils.listFiles(new File(PATH_OF_FILE), new String[]{"txt"}, true)) {
                doc = new Document();

                /* 把内容添加到索引域中，即为该文档存储信息，供将来搜索时使用(下面的写法，其默认为Field.Store.NO和
                   Field.Index.ANALYZED)，如果我们想把content的内容也存储到硬盘上，那就先要把file转换成字符串，然后按
                   "fileName"的存储方式加到Field中，我们可以用commons-io包中的FileUtils.readFileToString()转换
                 */
                //doc.add(new Field("content",FileUtils.readFileToString(file),Field.Store.YES,Field.Index.ANALYZED));
                doc.add(new Field("content", new FileReader(file)));
                /* Field.Store.YES          将文件的全名写入硬盘
                   Field.Index.NOT_ANALYZED 不对文件名进行分词
                 */
                doc.add(new Field("fileName", file.getName(), Field.Store.YES, Field.Index.NOT_ANALYZED));
                doc.add(new Field("filePath", file.getAbsolutePath(), Field.Store.YES, Field.Index.NOT_ANALYZED));
                // 通过IndexWriter添加文档到索引中
                writer.addDocument(doc);
            }
        } catch (IOException | NullPointerException e) {
            System.out.println("创建索引的过程中遇到异常，堆栈轨迹如下：");
            e.printStackTrace();
        } finally {
            if (null != writer) {
                try {
                    writer.close(); // IndexWriter在用完之后一定要关闭
                } catch (IOException e) {
                    System.out.println("关闭IndexWriter时遇到异常，堆栈轨迹如下：");
                    e.printStackTrace();
                }
            }
        }
    }

    //@formatter:off
    /**
     * ---------------------------------------------------------
     * 搜索文件
     *
     * 1. 创建Directory，指定索引被保存的位置
     * 2. 创建IndexReader，通过IndexReader读取索引
     * 3. 根据IndexReader创建IndexSearcher
     * 4. 创建搜索的Query
     * 5. 根据IndexSearcher搜索并返回TopDocs
     * 6. 根据TopDocs获取ScoreDoc对象
     * 7. 根据IndexSearcher和ScoreDoc对象获取具体的Document对象
     * 8. 根据Document对象获取需要的值
     * 9. 关闭IndexReader
     *
     * */
    public void searchFile() {
    //@formatter:on

        IndexReader reader = null;
        try {
            reader = IndexReader.open(FSDirectory.open(new File(PATH_OF_INDEX)));
            IndexSearcher searcher = new IndexSearcher(reader);
            /* 创建基于Parser搜索的Query,创建时需指定其"搜索的版本,默认搜索的域,分词器"....
               这里的域指的是创建索引时Field的名字
             */
            QueryParser parser = new QueryParser(Version.LUCENE_36, "content", new StandardAnalyzer(Version.LUCENE_36));
            // 指定==>搜索域为content(即上一行代码指定的"content")中包含"java"的文档
            Query query = parser.parse("lucene");
            // 第二个参数指定搜索后显示的条数,若查到5条则显示为5条,查到15条则只显示10条
            TopDocs tds = searcher.search(query, 10);
            // TopDocs中存放的并不是我们的文档,而是文档的ScoreDoc对象
            ScoreDoc[] sds = tds.scoreDocs;

            // ScoreDoc对象相当于每个文档的ID号,我们就可以通过ScoreDoc来遍历文档
            for (ScoreDoc sd : sds) {
                // sd.doc得到的是文档的序号
                Document doc = searcher.doc(sd.doc);
                // 输出该文档所存储的信息
                System.out.println(doc.get("fileName") + " [" + doc.get("filePath") + "]");
            }
        } catch (Exception e) {
            System.out.println("搜索文件的过程中遇到异常,堆栈轨迹如下：");
            e.printStackTrace();
        } finally {
            if (null != reader) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.out.println("关闭IndexReader时遇到异常,堆栈轨迹如下：");
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void test() {
        HelloWorld beginning = new HelloWorld();
        beginning.createIndex();
        beginning.searchFile();

    }

}
